<?php
/**
 * Celebrio Core
 * 
 * @copyright  Copyright (c) 2011 Celebrio Software
 * @package    Celebrio
 * @subpackage Core
 */
namespace Celebrio\Core;

use Nette\Object;
use Nette\Environment;

use \Celebrio\Util\CacheName;

/**
 * ManifestLoader finds all existing modules in the system.
 *
 * @author pavel
 */
class ManifestLoader extends Object {
    
    const MANIFEST = "cmf-";
    const MANIFEST_LIST = "manifests";
    
    
    /**
     * When the manifest search is performed, this array is filled by
     * all paths to the available manifests in the system.
     * 
     * @var array(string)
     */
    private $manifestPaths = array();
    
    /**
     * Gets all manifest paths found under the APP or LIBS dir. When
     * searching for the first time, the result is fetched and cached.
     * The next time, the cache contents is returned.
     */
    private function getManifestPaths() {
        
        $cache = CacheName::generate($this);
        if (!$cache->offsetExists(self::MANIFEST_LIST)) {
            $this->findManifests(Environment::expand("%appDir%"));
            $this->findManifests(Environment::expand("%libsDir%"));
            $cache->offsetSet(self::MANIFEST_LIST, $this->manifestPaths);
        } else {
            $this->manifestPaths = $cache->offsetGet(self::MANIFEST_LIST);
        }
        //\Nette\Debug::dump($this->manifests);
        return $this->manifestPaths;
    }
    
    /**
     * Creates the array of manifest objects from all known manifest files.
     * 
     * @return array(Manifest)
     */
    public function getAllManifests() {
        $mfPaths = $this->getManifestPaths();
        
        $manifests = array();
        foreach ($mfPaths as $manifestPath) {
            $manifests[] = new Manifest($manifestPath);
        }
        return $manifests;
    }

    /**
     * @param string $name
     * @return \Celebrio\Core\Manifest
     */
    public function findManifest($name) {
        $all_manifests = $this->getAllManifests();
        foreach ($all_manifests as $manifest) {
            if ($manifest->getFullNameDots() === $name) {
                return $manifest;
            }
        }
        return null;
    }
    
    /**
     * Returns only the modules which can be activated directly. It means
     * the abstract, service or application modules. In other words, these
     * are the modules displayed on the main page of module management.
     * 
     * @return array(Manifest)
     */
    public function getActivable() {
        $manifests = $this->getAllManifests();
        
        $activable = array();
        foreach ($manifests as $manifest) {
            if ($manifest->isActivable()) {
                $activable[] = $manifest;
            }
        }
        return $activable;
    }
    
    /**
     * Returns all manifests which claim they implement the interface
     * specified by the module which is given as parameter.
     * 
     * @param Manifest $interfaceModule The module specifying the interface
     * @return array(Manifest)
     */
    public function getImplementationsFor(Manifest $interfaceModule) {
        $manifests = $this->getAllManifests();
        $interface = $interfaceModule->getInterfaceFullName();
        
        $implementations = array();
        foreach ($manifests as $manifest) {
            if (($manifest->getInterfaceFullName() === $interface) && !$manifest->isAbstract()) {
                $implementations[] = $manifest;
            }
        }
        return $implementations;
    }
    
    /**
     * Returns the abstract module for the given module implementing the interface. 
     * 
     * @param Manifest $implementation
     * @return Manifest the abstract module manifest
     * 
     * @throws ManifestException if none or more than 1 module are found
     */
    public function getAbstract(Manifest $implementation) {
        if ( ! $implementation->isImpl()) {
            throw new ManifestException("Trying to find abstract for not-implementation.");
        }
        
        $manifests = $this->getAllManifests();
        $interface = $implementation->getInterfaceFullName();
        
        $abstracts = array();
        foreach ($manifests as $manifest) {
            if (($manifest->getInterfaceFullName() === $interface) && !$manifest->isImpl()) {
                $abstracts[] = $manifest;
            }
        }
        
        if (count($abstracts) === 1) {
            return $abstracts[0];
        } else {
            throw new ManifestException("Wrong number of abstracts found: " . \count($abstracts));
        }
    }
    
    /**
     * Gets the manifest for the module identified by the given parameter.
     * The parameter represents the module full name (namespace + identifier)
     * separated by "dots" - which are dashes actually.
     * 
     * @param string $fullNameDots for example TestNamespace-TestSubNs-TestModule
     * @return Manifest
     */
    public function getManifest($fullNameDots) {
        $manifests = $this->getAllManifests();
        
        foreach ($manifests as $manifest) {
            if ($manifest->getFullNameDots() === $fullNameDots) {
                return $manifest;
            }
        }
        throw new ManifestException("Module with name $fullNameDots not found");
    }
    
    
//-----------------------------------------------------------------------------//
//-----------------------------------------------------------------------------//
//-----------------------------------------------------------------------------//
//-----------------------------------------------------------------------------//
    
    
    
    /**
     * Recursively finds all instances of "cmf-*" file (the name is
     * specified by the class constant) and sets them (as absolute paths).
     * 
     * The method opens the directory and loops through it. When another
     * directory (i.e subdirectory) is found, the metod is called again,
     * recursively with the subdirectory as argument. Files/directories
     * specified by getIgnored() are ignored (what a surprise).
     * 
     * @param string $path Path to the directory we want to search in.
     */
    private function findManifests($path){
        
        $dh = @opendir( $path );
        
        while (false !== ($file = readdir($dh))) { // read contents
            if (!in_array($file, $this->getIgnored())) { // is ignored? => skip
                if (is_dir( "$path/$file")) { // is directory?
                    $this->findManifests( "$path/$file"); // search recursively
                } else if (\substr($file, 0, \strlen(self::MANIFEST)) === self::MANIFEST) { // is manifest file?
                    $this->manifestPaths[] = $path . "/" . $file;
                }
            }
        }
        closedir($dh);
    }
    
    /**
     * @return array(string) list of the files/dirs which should be ignored
     */
    private function getIgnored() {
        return array( "cgi-bin", ".", "..", "temp", ".svn" );
    }
    
}

